﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Magiq.Query;
using Magiq.Support;
using Magiq.Update;
using Magiq.Delete;
using Magiq.Insert;

namespace Magiq
{
    public static class MagiqExtensions
    {
        public static IUpdate<T> Set<T, TReturn>(this IEnumerable<T> self, Expression<Func<T, TReturn>> property,
                                                 TReturn value)
            where T : class
        {
            return new Update<T>(self.Validate()).Set(property, value);
        }

        public static IUpdate<T> Set<T, TReturn>(this IEnumerable<T> self, Expression<Func<T, TReturn>> property,
                                                 Expression<Func<T, TReturn>> value) where T : class
        {
            return new Update<T>(self.Validate()).Set(property, value);
        }

        public static int Delete<T>(this IEnumerable<T> self) where T : class
        {
            return new Delete<T>(self.Validate()).Execute();
        }

        public static int Delete<T>(this IEnumerable<T> self, Expression<Func<T, bool>> where) where T : class
        {
            return new Delete<T>(self.Validate().Where(where)).Execute();
        }

        private static IQueryable<T> Validate<T>(this IEnumerable<T> self)
        {
            if (!(self is IQueryable<T>))
                throw new InvalidOperationException(
                    "Magiq methods should be called to instances of IQueryable<T>. Use another override. (IEnumerable<T> support is just for expression parsing and is not intented to be called from your code directly)");

            return (IQueryable<T>) self;
        }
    
        private static readonly MethodInfo[] UpdateMethods = typeof(MagiqExtensions).GetMethods()
               .Where(x => x.Name == "Set")
               .ToArray();

        internal static IQueryable<TSelect> GetQueryable<TSource, TChild, TSelect>(TSource self, Expression<Func<TSource, IEnumerable<TChild>>> collection, Expression<Func<TChild, TSelect>> select) where TSource : class
        {
            var split = ExpressionSplit<TSource, TChild>.From(collection);

            var strategy = MagiqConfigurator.ForQuery(new QueryInfo<TSource, TChild>(self, split.PropertyExpression));

            var query = strategy.Execute();

            if (split.WhereExpression != null)
                query = query.Where(split.WhereExpression);

            return query.Select(select);
        }

        public static IQueryable<TChild> Query<T, TChild>(this T self, Expression<Func<T, IEnumerable<TChild>>> collection)
            where T : class
        {
            var selectSplit = ExpressionSelectSplit.From(collection.Body);
            if (selectSplit.Select == null)
            {
                var split = ExpressionSplit<T, TChild>.From(collection);

                var strategy = MagiqConfigurator.ForQuery(new QueryInfo<T, TChild>(self, split.PropertyExpression));

                var query = strategy.Execute();

                if (split.WhereExpression != null)
                    query = query.Where(split.WhereExpression);

                return query;
            }

            var method = typeof(MagiqExtensions).GetMethod("GetQueryable", BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(typeof (T), selectSplit.Select.Body.Type, typeof (TChild));

            return (IQueryable<TChild>) method.Invoke(null, new object[] {self, Expression.Lambda(selectSplit.Rest.WrapInEnumerable(), collection.Parameters), selectSplit.Select});
        }

        public static int DeleteCollection<T, TChild>(this T self, Expression<Func<T, IEnumerable<TChild>>> collection)
            where T : class
            where TChild : class
        {
            return new Delete<TChild>(self.Query(collection)).Execute();
        }

        public static int UpdateCollection<T, TChild>(this T self, Expression<Func<T, IUpdate<TChild>>> collection)
            where T : class
            where TChild : class
        {
            var operations = new List<Expression[]>();

            MethodCallExpression call;
            var rest = collection.Body;
            var contunue = true;

            //Navigates the expression until not more sets are being called.
            while (contunue)
            {
                call = rest as MethodCallExpression;
                if (call != null && call.Method.GetGenericMethodDefinition().In(UpdateMethods))
                {
                    operations.Add(call.Arguments.Skip(1).ToArray());
                    rest = call.Arguments[0];
                }
                else
                {
                    contunue = false;
                }
            }

            var query = self.Query(Expression.Lambda<Func<T,IEnumerable<TChild>>>(rest, collection.Parameters.ToArray()));

            var update = new Update<TChild>(query);

            foreach (var operation in operations)
            {
                LambdaExpression valueExpression;
                var property = ((UnaryExpression)operation[0]).Operand;
            
                if (operation[1] is ConstantExpression)
                    valueExpression = Expression.Lambda(operation[1], Expression.Parameter(typeof(T), "x"));
                else if (operation[1] is UnaryExpression)
                    valueExpression = (LambdaExpression)((UnaryExpression)operation[1]).Operand;
                else
                    throw new InvalidOperationException("Expression contains unknown methods for update.");

                update.SetByLambdas((LambdaExpression)property, valueExpression);
            }

            return update.Execute();
        }

        public static int Insert<T, TNew>(this IQueryable<T> self, IQueryable<TNew> source)
            where T : class
            where TNew : T
        {
            return new InsertSource<TNew>(source).CreateInsert(self).Execute();
        }

        public static int BulkInsert<T, TNew>(this IQueryable<T> self, IQueryable<TNew> source)
            where T : class
            where TNew : T
        {
            return new InsertSource<TNew>(source).CreateInsert(self).Execute();
        }
        public static int BulkInsertInto<T, TDestination, TNew>(this T self, Expression<Func<T, IEnumerable<TDestination>>> into, IQueryable<TNew> values)
            where TDestination : class
            where T : class
            where TNew : TDestination
        {
            var destination = self.Query(into);
            return destination.BulkInsert(values);
        }
        public static int InsertInto<T, TDestination, TNew>(this T self, Expression<Func<T, IEnumerable<TDestination>>> into, IQueryable<TNew> values)
            where TDestination : class
            where T : class
            where TNew : TDestination
        {
            var destination = self.Query(into);
            return destination.Insert(values);
        }
    }
}